﻿using System;
using System.Linq;

namespace NeuralLib.Learning
{
    public class KohonenPackLearning : KohonenLearning
    {
        public event EventHandler NetworkLearnCompleted;

        public event EventHandler NetworkEpochCompleted;

        public KohonenPackLearning(KohonenNetwork net, int iterationsCount)
        {
            _network = net;
            _iterationsCount = iterationsCount;
            _initialEfficientWidth = _efficientWidth = Math.Min(net.KmLayer.NeuronsXCount, net.KmLayer.NeuronsYCount) / 2;
            _efficientWidthScaleSpeed = iterationsCount / _initialEfficientWidth;
            _initialLearningRate = _learningRate = 0.1;
            _learningRateScaleSpeed = 1000;
        }

        public override void Run(double[][] input)
        {
            var samplesCount = input.Count();
            for (var i = 0; i < _iterationsCount; i++)
            {
                _winningRates = new double[_network.KmLayer.NeuronsCount];
                _winnersIndexes = new int[samplesCount];
                for (var j = 0; j < samplesCount; j++)
                {
                    var winnerInd = _network.KmLayer.Winner(input[j]);
                    _winnersIndexes[j] = winnerInd;
                    _winningRates[winnerInd]++;
                }

                var avX = GetAverageInputs(input);
                WeightsUpdate(avX);
                UpdateEfficientWidth(i + 1);
                UpdateLearningRate(i + 1);
                if (NetworkEpochCompleted != null)
                    NetworkEpochCompleted(null, new EventArgs());
            }

            var error = CalculateError(input);
            var deads = DeadNeurons(input);
            if (NetworkLearnCompleted != null)
                NetworkLearnCompleted(null, new LearnerEventArgs(error, deads));
        }

        private double[][] GetAverageInputs(double[][] inputs)
        {
            var neuronsCount = _network.KmLayer.NeuronsCount;
            var averageInputs = new double[neuronsCount][];
            var inputLength = inputs[0].Length;
            var inputsCount = inputs.GetLength(0);

            for (var i = 0; i < neuronsCount; i++)
                averageInputs[i] = new double[inputLength];

            for (var i = 0; i < inputsCount; i++)
                for (var j = 0; j < inputLength; j++)
                    averageInputs[_winnersIndexes[i]][j] += inputs[i][j];

            for (var i = 0; i < neuronsCount; i++)
                for (var j = 0; j < inputLength; j++)
                    if (_winningRates[i] != 0)
                        averageInputs[i][j] /= _winningRates[i];
            return averageInputs;
        }

        private void WeightsUpdate(double[][] averageInputs)
        {
            var neuronsXCount = _network.KmLayer.NeuronsXCount;
            var neuronsYCount = _network.KmLayer.NeuronsYCount;
            var weightsCount = _network.KmLayer.Neurons[0].Weights.Count();

            for (var i = 0; i < neuronsXCount; i++)
                for (var j = 0; j < neuronsYCount; j++)
                {
                    var curIndex = i * neuronsYCount + j;
                    var neuron = _network.KmLayer.Neurons[curIndex];

                   
                    var neighbours = _network.KmLayer.Neurons.Where(n => (Math.Abs(n.Coords.X - neuron.Coords.X) <= _efficientWidth && Math.Abs(n.Coords.Y - neuron.Coords.Y) <= _efficientWidth)).ToList();

                    for (var t = 0; t < weightsCount; t++)
                    {
                        double weightedDelta = 0, delta = 0;
                      
                        foreach (var neighbour in neighbours)
                        {                            
                            var neibIndex = Array.IndexOf(_network.KmLayer.Neurons, neighbour);
                            var temp = _winningRates[neibIndex] * NeuronsDistance(neighbour, neuron);
                            delta += temp;
                            weightedDelta += temp * averageInputs[neibIndex][t];
                        }
                        if (delta!=0)
                        neuron.Weights[t] = weightedDelta / delta;
                    }
                }
        }
    }

    public class LearnerEventArgs : EventArgs
    {
        public LearnerEventArgs(double error, int[] dead)
        {
            Error = error;
            DeadNeurons = dead;
        }

        public int[] DeadNeurons { get; internal set; }
        public double Error { get; internal set; }
    }
}